﻿
using System;
using System.Net;
using System.Net.Sockets;
using System.Windows.Threading;
namespace XDCCManager.Network.IRC
{
	public sealed class ChatConnection : IChatConnection
	{
		static ChatConnection()
		{
			Instance = new ChatConnection();
		}

		private ChatConnection()
		{
			ServerConnection.Instance.MessageReceived += ServerConnection_MessageReceived;
			writeMessageDelegate = new Action<string>(WriteMessage);
			readBuffer = new byte[1024];
			readTokenizer = new ChatLineTokenizer(1024);
			ConnectionEncoding = System.Text.Encoding.GetEncoding("ISO-8859-1");
		}

		private Action onConnectSuccess;
		private Action<string> onConnectFailure;
		private Action<ConnectionStateEventArgs> onStateUpdateNotify;
		private Dispatcher writeDispatcher;
		private Dispatcher readDispatcher;
		private System.IO.Stream botStream;
		private byte[] readBuffer;
		private ChatLineTokenizer readTokenizer;
		private Action<string> writeMessageDelegate;
		private bool closeRequested;

		public bool Connected { get; private set; }

		public bool Connecting { get; private set; }

		public System.Text.Encoding ConnectionEncoding { get; set; }

		public static ChatConnection Instance { get; private set; }

		public PackUpdatesManager Manager { get; set; }

		internal void Open(Action successAction, Action<string> failureAction, Action<ConnectionStateEventArgs> stateUpdateNotify)
		{
			if (Connected || Connecting)
				return;
			onConnectSuccess = successAction;
			onConnectFailure = failureAction;
			onStateUpdateNotify = stateUpdateNotify;
			closeRequested = false;
			Connecting = true;
			SendChatRequest();
		}

		internal void Close()
		{
			if (!Connected || !Connecting)
				return;
			closeRequested = true;
			CloseStreamAndDispatchers();
			Connecting = false;
			Connected = false;
			if (ConnectionClosed != null)
				ConnectionClosed();
		}

		#region Connection establishment
		private void SendChatRequest()
		{
			XDCCManager.Model.Servers.Server server = ServerConnection.Instance.Server;
			ServerConnection.Instance.SendMessage(IRCEngine.Message.BuildPrivMsgMessage(server.BotName, string.Format(ChatMessages.RequestChat, server.Password)));
			onStateUpdateNotify(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.RequestingChat));
		}

		void ServerConnection_MessageReceived(object sender, MessageEventArgs e)
		{
			if ((e.Message.Prefix == null) || (e.Message.Prefix.Nick != ServerConnection.Instance.Server.BotName))
				return;
			string middle = IRCEngine.MessageConverter.LowLevelDecode(e.Message.Params.TrailingParam);
			string client = IRCEngine.MessageConverter.CTCPDecode(IRCEngine.MessageConverter.RemoveCTCPMessages(middle));
			string[] ctcp = IRCEngine.MessageConverter.ExtractCTCPMessages(middle);
			if (Connecting)
			{
				System.Text.RegularExpressions.Match match;
				switch (e.Message.Command.CommandName)
				{
					case "NOTICE":
						if ((match = ChatMessages.ResponseAdmin.Match(client)).Success)
						{
							onConnectFailure("The user answered: " + match.Groups["reason"].Value);
							Connecting = false;
						}
						break;
					case "PRIVMSG":
						foreach (var request in ctcp)
						{
							string[] tokens = request.Split(' ');
							if (tokens.Length < 5)
								continue;
							if ((tokens[0] == "DCC") && (tokens[1] == "CHAT"))
							{
								OpenConnection(tokens);
							}
						}
						break;
				}
			}
		}

		private void OpenConnection(string[] tokens)
		{
			string addressValue = tokens[3];
			string portValue = tokens[4];
			int port = 0;
			if (!int.TryParse(portValue, out port))
			{
				onConnectFailure("Invalid CHAT request from Bot. Port is not an integer.");
				Connecting = false;
				return;
			}
			IPAddress address = null;
			try
			{
				address = Dns.GetHostAddresses(addressValue)[0];
			}
			catch (SocketException e)
			{
				onConnectFailure("Invalid CHAT request from bot. Address is invalid.\nMessage: " + e.Message);
				Connecting = false;
				return;
			}
			Socket socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			try
			{
				socket.BeginConnect(new IPEndPoint(address, port), ConnectCallback, socket);
			}
			catch (SocketException e)
			{
				onConnectFailure("Error connecting to Bot.\nMessage: " + e.Message);
				Connecting = false;
			}
		}
		#endregion

		#region Connection validation and reading
		private void ConnectCallback(IAsyncResult result)
		{
			Socket socket = result.AsyncState as Socket;
			try
			{
				socket.EndConnect(result);
			}
			catch (SocketException e)
			{
				onConnectFailure("Error connecting to Bot.\nMessage: " + e.Message);
				Connecting = false;
				return;
			}
			botStream = new NetworkStream(socket, true);
			(new System.Threading.Thread(new System.Threading.ThreadStart(() =>
			{
				System.Threading.Thread.CurrentThread.IsBackground = true;
				lock (this)
				{
					readDispatcher = Dispatcher.CurrentDispatcher;
				}
				Dispatcher.Run();
			}))).Start();
			(new System.Threading.Thread(new System.Threading.ThreadStart(() =>
			{
				System.Threading.Thread.CurrentThread.IsBackground = true;
				lock (this)
				{
					writeDispatcher = Dispatcher.CurrentDispatcher;
				}
				Dispatcher.Run();
			}))).Start();
			ChatState state = new ChatState();
			state.Step = ChatState.ChatStep.Start;
			System.Threading.Thread.Sleep(0);
			onStateUpdateNotify(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.Identifying));
			Read(state);
		}

		private void Read(ChatState state)
		{
			try
			{
				botStream.BeginRead(readBuffer, 0, readBuffer.Length, ReadCallback, state);
			}
			catch (System.IO.IOException)
			{
				lock (this)
				{
					if (closeRequested)
						return;
				}
				CloseStreamAndDispatchers();
				SendChatRequest();
				return;
			}
			catch (NullReferenceException)
			{
				lock (this)
				{
					if (closeRequested)
						return;
				}
				CloseStreamAndDispatchers();
				SendChatRequest();
				return;
			}
		}

		private void CloseStreamAndDispatchers()
		{
			lock (this)
			{
				if (readDispatcher != null)
				{
					readDispatcher.BeginInvokeShutdown(DispatcherPriority.Send);
					readDispatcher = null;
				}
				if (writeDispatcher != null)
				{
					writeDispatcher.BeginInvokeShutdown(DispatcherPriority.Send);
					writeDispatcher = null;
				}
				if (botStream != null)
				{
					botStream.Close();
					botStream = null;
				}
				readTokenizer.Clear();
			}
		}

		private void ReadCallback(IAsyncResult result)
		{
			ChatState state = result.AsyncState as ChatState;
			int read = 0;
			try
			{
				read = botStream.EndRead(result);
			}
			catch (System.IO.IOException)
			{
				lock (this)
				{
					if (closeRequested)
						return;
				}
				CloseStreamAndDispatchers();
				SendChatRequest();
				return;
			}
			catch (NullReferenceException)
			{
				lock (this)
				{
					if (closeRequested)
						return;
				}
				CloseStreamAndDispatchers();
				SendChatRequest();
				return;
			}
			ExtractMessages(read, state);
			if (state.Step != ChatState.ChatStep.Fail)
				Read(state);
		}

		private void ExtractMessages(int read, ChatState state)
		{
			readTokenizer.Append(readBuffer, read);
			string line = readTokenizer.GetLine(ConnectionEncoding);
			while (line != null)
			{
				if (line.Length > 0)
				{
					System.Diagnostics.Debug.WriteLine("<{0}", (object)line);
					if (ParseMessage(line, state))
						if (MessageReceived != null)
							readDispatcher.BeginInvoke(MessageReceived, this, new LineEventArgs(line));
					if (state.Step == ChatState.ChatStep.Fail)
						return;
				}
				line = readTokenizer.GetLine(ConnectionEncoding);
			}
		}

		private bool ParseMessage(string line, ChatState state)
		{
			switch (state.Step)
			{
				case ChatState.ChatStep.Start:
					if (line == ChatMessages.ResponseEnterPassword)
					{
						SendMessage(ServerConnection.Instance.Server.Password + '\n');
						state.Step = ChatState.ChatStep.PasswordSent;
						return false;
					}
					break;
				case ChatState.ChatStep.PasswordSent:
					if (line == ChatMessages.ResponseIncorrectPassword)
					{
						state.Step = ChatState.ChatStep.Fail;
						Connecting = false;
						CloseStreamAndDispatchers();
						return false;
					}
					else
					{
						state.Step = ChatState.ChatStep.Ready;
						onConnectSuccess();
						Connected = true;
						Connecting = false;
						if (ConnectionReady != null)
							ConnectionReady();
					}
					break;
				case ChatState.ChatStep.Ready:
					break;
			}
			return true;
		}
		#endregion

		#region Write to connection
		public void SendMessage(string message)
		{
			lock (this)
			{
				if (writeDispatcher != null)
				{
					writeDispatcher.BeginInvoke(writeMessageDelegate, message);
				}
			}
		}

		private void WriteMessage(string message)
		{
			try
			{
				System.Diagnostics.Debug.WriteLine(">{0}", (object)message);
				byte[] content = ConnectionEncoding.GetBytes(message);
				botStream.Write(content, 0, content.Length);
				botStream.Flush();
			}
			catch (System.IO.IOException)
			{
			}
			catch (NullReferenceException)
			{
			}
		}
		#endregion

		public event EventHandler<LineEventArgs> MessageReceived;
		public event Action ConnectionReady;
		public event Action ConnectionClosed;
	}
}
